package daobeauty.interceptor;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.text.Collator;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.SessionFactory;
import org.hibernate.metadata.ClassMetadata;

import daobeauty.dao.SessionFactoryAware;
import daobeauty.util.MethodSignatureUtils;

// TODO use this, and integrate into interceptor and invoke strategies
public class DaoMetaData {
	private static final Log LOG = LogFactory.getLog(DaoMetaData.class);
	
	// list of methodnames that will be ignored on the delegate objects 
	private static final HashSet<String> ignoredMethods = new HashSet<String>();
	static{
		ignoredMethods.add("getClass");
		ignoredMethods.add("wait");
		ignoredMethods.add("notify");
		ignoredMethods.add("notifyAll");
		ignoredMethods.add("toString");
		ignoredMethods.add("equals");
		ignoredMethods.add("hashCode");
		ignoredMethods.add("clone");
		ignoredMethods.add("compareTo");
		ignoredMethods.add("get");
		ignoredMethods.add("set");
		ignoredMethods.add("add");
		ignoredMethods.add("addAll");
	}
	
	private final SessionFactory sessionFactory;
	
	// domain meta data
	private final Class domainClass;
	// lasses of properties
	private final Map<String, Class> domainPropertyClassesByPropertyName = new TreeMap<String, Class>(Collator.getInstance());
	private final Class<? extends Serializable> domainPrimaryKeyClass;
	private final String domainPrimaryKeyPropertyName;
	
	// DAO meta data
	private final HashMap<String, Method> daoMethodsByMethodSignature = new HashMap<String, Method>(); // signature string to method of dao
	private final HashMap<Method, String> daoMethodSignaturesByMethod = new HashMap<Method, String>();
	
	// delegate meta data
	private final HashMap<String, Set<Object>> daoDelegatesByMethodSignature = new HashMap<String, Set<Object>>(); // delegates by method signature
	private final HashSet<Object> daoDelegates = new HashSet<Object>();
	
	public DaoMetaData(final Class domainClass,	final Collection<Class> daoInterfaces,
			final Collection<Object> daoMethodDelegates, final SessionFactory sessionFactory) {
		this(
			domainClass, 
			(Class[])daoInterfaces.toArray(new Class[daoInterfaces.size()]),
			daoMethodDelegates!=null?daoMethodDelegates.toArray(new Object[daoMethodDelegates.size()]):null,
			sessionFactory
		);
	}	
	
	/**
	 * @param domainClass the domain class
	 * @param daoInterfaces interfaces implemented by the dao
	 * @param daoMethodDelegates delegate objects
	 * @param sessionFactory the sessionfactory
	 */
	@SuppressWarnings("unchecked")
	public DaoMetaData(final Class domainClass,	final Class[] daoInterfaces,
			final Object[] daoMethodDelegates, final SessionFactory sessionFactory) {
		super();
		// TODO illegal argument exceptions
		this.domainClass = domainClass;
		this.sessionFactory = sessionFactory;
		
		// collect all methods and signatures of dao
		daoMethodsByMethodSignature.putAll(MethodSignatureUtils.toMethodSignatureMap(daoInterfaces));
		for (String signature:daoMethodsByMethodSignature.keySet()){
			if (LOG.isDebugEnabled()){
				LOG.debug("found dao method: "+signature);
			}
			daoMethodSignaturesByMethod.put(daoMethodsByMethodSignature.get(signature), signature);
		}
		
		
		if (daoMethodDelegates!=null && daoMethodDelegates.length>0){
			if (LOG.isDebugEnabled()){
				LOG.debug("found "+daoDelegates.size()+" delegates");
			}

			
			// determine all methods of the delegates without the ones for
			// the SessionFactoryAware interface
			// and collect a map for signatures -> delegates
			final Set<String> sessionFactoryAwareMethodSignatures = MethodSignatureUtils.toMethodSignature(SessionFactoryAware.class.getMethods()); // to exclude them
			Set<Object> delegatesForMethodSignature;
			Set<String> delegateMethodSignatures;
			for (Object daoDelegate:daoMethodDelegates){
				if (LOG.isDebugEnabled()){
					LOG.debug("processing delegate: "+daoDelegate);
				}
				
				delegateMethodSignatures = MethodSignatureUtils.toMethodSignature(daoDelegate.getClass(), ignoredMethods);
				delegateMethodSignatures.removeAll(sessionFactoryAwareMethodSignatures);
				
				for (String delegateMethodSignature:delegateMethodSignatures){
					delegatesForMethodSignature = daoDelegatesByMethodSignature.get(delegateMethodSignature);
					if (delegatesForMethodSignature==null){
						delegatesForMethodSignature = new HashSet<Object>();
						daoDelegatesByMethodSignature.put(delegateMethodSignature, delegatesForMethodSignature);
					}
					delegatesForMethodSignature.add(daoDelegate);
					if (LOG.isDebugEnabled()){
						LOG.debug("adding delegate method "+delegateMethodSignature);
					}
				}
			}
			
			this.daoDelegates.addAll(daoDelegates);
		}
		else{
			if (LOG.isDebugEnabled()){
				LOG.debug("no delegates found");
			}
		}
		
		// initialize Domain Meta Data
		final ClassMetadata cmd = sessionFactory.getClassMetadata(domainClass);
		domainPrimaryKeyClass = cmd.getIdentifierType().getReturnedClass();
		domainPrimaryKeyPropertyName = cmd.getIdentifierPropertyName();
		domainPropertyClassesByPropertyName.put(domainPrimaryKeyPropertyName, domainPrimaryKeyClass);
		if (LOG.isDebugEnabled()){
			LOG.debug("hibernate identifier property: "+domainPrimaryKeyClass);
		}
		final String [] propertyNames = cmd.getPropertyNames();
		if (propertyNames!=null){
			for (int i=0; i<propertyNames.length; i++){
				domainPropertyClassesByPropertyName.put(propertyNames[i], cmd.getPropertyType(propertyNames[i]).getReturnedClass());
				LOG.debug("hibernate property: "+propertyNames[i]);
			}
		}
	}
	
	
	public SessionFactory getSessionFactory() {
		return sessionFactory;
	}
	
	public Class getDomainClass() {
		return domainClass;
	}
	
	public Set<String> getDomainProperties() {
		return Collections.unmodifiableSet(domainPropertyClassesByPropertyName.keySet());
	}
	
	public Map<String, Class> getDomainPropertyClasses() {
		return Collections.unmodifiableMap(domainPropertyClassesByPropertyName);
	}
	
	public Class getDomainPropertyClass(String propertyName){
		return domainPropertyClassesByPropertyName.get(propertyName);
	}
	
	/**
	 * Returns the class of the primary key
	 */
	public Class<? extends Serializable> getDomainPrimaryKeyClass() {
		return domainPrimaryKeyClass;
	}
	
	/**
	 * Returns the property name of the primary key
	 */
	public String getDomainPrimaryKeyPropertyName() {
		return domainPrimaryKeyPropertyName;
	}
	
	public String getDaoMethodSignatureByMethod(Method daoMethod){
		return daoMethodSignaturesByMethod.get(daoMethod);
	}
	
	public Method getDaoMethodByMethodSignature(String signature){
		return daoMethodsByMethodSignature.get(signature);
	}
	
	public Set<String> getDaoMethodSignatures() {
		return Collections.unmodifiableSet(daoMethodsByMethodSignature.keySet());
	}
	
	public Set<Method> getDaoMethods() {
		return Collections.unmodifiableSet(daoMethodSignaturesByMethod.keySet());
	}
	
	/**
	 * Returns the delegate objects for this dao
	 */
	public Set<Object> getDaoDelegates() {
		return Collections.unmodifiableSet(daoDelegates);
	}
	
	public Set<Object> getDaoDelegatesByMethodSignature(String daoMethodSignature){
		return daoDelegatesByMethodSignature.get(daoMethodSignature);
	}
	
	public Set<Object> getDaoDelegatesByMethod(Method daoMethod){
		return daoDelegatesByMethodSignature.get(getDaoMethodSignatureByMethod(daoMethod));
	}
	
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((domainClass == null) ? 0 : domainClass.hashCode());
		return result;
	}
	
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		final DaoMetaData other = (DaoMetaData) obj;
		if (domainClass == null) {
			if (other.domainClass != null)
				return false;
		} else if (!domainClass.equals(other.domainClass))
			return false;
		return true;
	}
}
